Skip to content

S13-06 React-Hooks

[TOC]

API

React

  • useState(initialState?)返回:[state, setState],用于在函数组件中管理状态(state)

    • 参数
    • initialState?any,状态的初始值,不设置则为undefined
    • 返回
    • state:``,当前状态值
    • setState:``,用于更新状态值的函数。当使用这个函数设置新的状态时,React会根据新的状态重新渲染组件。
  • useEffect(callback, dependencies?)返回:,用于在函数组件中执行副作用操作,例如访问API、订阅事件、设置定时器等,可以模拟生命周期

    • 参数
    • callback() => void | callbackFn,要执行的副作用代码,可以是异步的或同步的
      • 返回
      • EffectCallback,在useEffect()函数中返回一个函数,这个函数将在组件卸载时执行。
    • dependencies?[],包含了与回调函数有关的依赖项,当这些依赖项发生变化时,才会重新执行回调函数。如果该数组为空,则仅在组件挂载和卸载时运行一次副作用代码。
  • useContext(MyContext)返回:value,可以实现组件之间的数据共享。让你在组件树中传递数据,而不需要手动的将props一级一级地传递下去。

    • 参数
    • MyContextContext,一个Context对象,它可以通过React.createContext()来创建。
    • 返回
    • value:``,返回该Context对象的当前值。
  • useReducer(reducer, initialState)返回:[state, dispatch],提供了一种可预测且可测试的方式来处理状态更新逻辑。

    • 参数
    • reducer(state, action) => void,用于根据当前状态和传递的 action 返回新的状态
    • initialStateany,状态的初始值
    • 返回
    • state:``,当前状态
    • dispatch(action) => void,用来分发 action 来触发状态更新
  • useCallback(callback, deps)返回:memorizedCallback,用于返回一个memorized(记忆化)的回调函数。可用于提高性能

    • 参数
    • callbackFunction,要记忆化的回调函数
    • depsArray,是一个依赖数组,当依赖数组中的任意一个值发生变化时,callback就会重新生成。
    • 返回
    • memorizedCallbackFunction,memoized(记忆化)的回调函数
  • useMemo(callback, deps)返回:memoizedValue,用于缓存计算结果,以便在依赖项未更改时避免不必要的重新计算。

    • 参数
    • callbackFunction,计算逻辑的回调函数
    • depsArray,是一个依赖数组,当依赖项数组中的任何一个值发生变化时,useMemo()会重新计算并返回新的计算结果。如果依赖项数组为空,则该函数仅在组件首次渲染时被计算。
    • 返回
    • memoizedValueFunction,memoized(记忆化)的计算值
  • useRef(initialValue?)返回:refContainer,用于创建一个可变的引用,它返回一个对象,对象中有一个 current 属性,该属性的值可以在组件的整个生命周期中保持不变,并可以被读取和修改。

    • 参数
    • initialValue?any,表示 refContainer.current 的初始值
    • 返回
    • refContainer{current},refContainer对象中有一个 current 属性,该属性的值可以在组件的整个生命周期中保持不变,并可以被读取和修改。
  • useImperativeHandle(ref, createHandle, deps?)返回:,用于向父组件暴露子组件的实例方法

    • 参数
    • refObject | (cpn) => void,如果 ref 参数是函数,则函数的第一个参数为子组件的实例对象,用于设置 ref 的值。
    • createHandle() => {method,...},用于创建一个对象,该对象包含子组件需要向父组件暴露的方法。
    • deps?array,包含所有影响 createHandle 函数的值,当这些值发生变化时,会重新调用 createHandle 函数。
  • useLayoutEffect(effect, deps)返回:,与 useEffect() 类似,但是它会在浏览器 layout 和 paint 之前执行,因此可以在渲染前同步读取 DOM 布局和触发重渲染。

    • 参数
    • effect() => void | Function,用于执行副作用操作,可以返回一个清除函数。
    • depsarray,包含所有影响 effect 函数的值,当这些值发生变化时,会重新调用 effect 函数。
  • useId(prefix?)返回:id,用于生成横跨服务端和客户端的稳定的唯一ID的同时避免 hydration 不匹配的 hook

    • 参数
    • prefixstring,用于在生成的id前添加前缀。如果不传入prefix,则默认为“id”。
    • 返回
    • idstring,生成的唯一标识符
  • useTransition({timeoutMs})返回:[isPending, startTransition],用于在渲染期间对异步更新进行控制。告诉 react 对于某部分任务的更新优先级较低,可以稍后进行更新。

    • 参数
    • timeoutMsnumber,表示等待异步更新的最长时间,超过这个时间后,异步更新将被强制中止。
    • 返回
    • isPendingboolean,,表示当前是否处于异步更新的等待中。
    • startTransition(callback) => void,用于触发异步更新
  • useDeferredValue(value, config)返回:deferredValue,接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后

    • 参数

    • value:``,当前状态的值

    • config:``,配置项

      • timeoutMs:``,定义延迟多少毫秒后开始更新状态的时间,默认为500毫秒。
      • equals:``,定义判断两个值相等的回调函数,如果该函数返回true,React会认为两个值相等,不再更新状态。
    • 返回

    • deferredValue:``,是一个延迟更新的值

React Redux

  • useSelector(selectorFn, equalityFn?)返回:selectedState,用于从 Redux store 中获取 state。
    • 参数
    • selectorFn(state) => selectedState,接收整个 Redux store 的 state 作为参数,返回需要获取的 state。
    • equalityFn?Function,比较返回的两个对象是否相等来决定是否组件重新渲染
    • 返回
    • selectedStateState,Redux store 中的 state
  • useDispatch()返回:dispatch,用于获取 dispatch 函数,从而可以向 Redux store 发送 action。
    • 返回
    • dispatch(action) => void,dispatch 函数,可以用来发送 action
  • useStore()返回:store,用于获取 Redux store 对象。它返回整个 Redux store 对象,可以用来获取、设置 state,以及订阅 state 的变化。
    • 返回
    • storeObject,整个 Redux store 对象

认识Hooks

为什么需要Hook?

Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。

我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:

  • class组件可以定义自己的state,用来保存组件自己内部的状态

    函数式组件不可以,因为函数每次调用都会产生新的临时变量;

  • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次

    函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求

  • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;

    函数式组件在重新渲染时整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;

所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。

类组件修改状态

js
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
+      count: 100
    }
  }
  render() {
    const { count } = this.state
    return (
      <div>
        <div>App Counter: {count}</div>
+        <button onClick={e => this.setState({ count: count + 1 })}> +1 </button>
      </div>
    )
  }
}

函数组件的缺点

  • 组件不会被重新渲染:修改message后,组件不知道要重新渲染
  • 如果页面重新渲染:函数会被重新执行,第二次执行时,会重新给message赋值为 'Hello World'
  • 也没有生命周期函数
js
const App = memo(() => {
+  let message = 'Hello World'
  return (
    <div>
      <div>App Counter: {message}</div>
+      <button onClick={e => message = '你好,世界'}> 修改msg </button>
    </div>
  )
})

说明:点击“修改msg” 并不能重新渲染页面

Class组件存在的问题

1、复杂组件变得难以理解:

  • 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;

  • 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount 中移除);

  • 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;

2、难以理解的class:

  • 很多人发现学习ES6的class是学习React的一个障碍。

  • 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;

  • 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;

3、组件复用状态很难

  • 在前面为了一些状态的复用我们需要通过高阶组件;

  • 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;

  • 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套

  • 这些代码让我们不管是编写和设计上来说,都变得非常困难;

Hook的出现

Hook的出现,可以解决上面提到的这些问题;

简单总结一下hooks:

  • 它可以让我们在不编写class的情况下使用state以及其他的React特性

  • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;

Hook的使用场景:

  • Hook的出现基本可以代替我们之前所有使用class组件的地方

  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;

  • Hook只能函数组件中使用,不能类组件,或者函数组件之外的地方使用;

在我们继续之前,请记住 Hook 是:

  • 完全可选的**:**你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。

  • 100% 向后兼容的**:**Hook 不包含任何破坏性改动。

  • 现在可用**:**Hook 已发布于 v16.8.0。

Class组件和Functional组件对比

image-20230404115236252

计数器案例对比

我们通过一个计数器案例,来对比一下class组件和函数式组件结合hooks的对比:

类组件

js
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
+      count: 100
    }
  }
  render() {
    const { count } = this.state
    return (
      <div>
+        <div>App Counter: {count}</div>
+        <button onClick={e => this.setState({ count: count + 1 })}> +1 </button>
      </div>
    )
  }
}

函数组件

js
const App = memo(() => {
  // 1. 通过useState定义count, setCount
+  const [count, setCount] = useState(100)

  return (
    <div>
      {/* 2. 显示count */}
+      <div>App Counter: {count}</div>
      {/* 3. 修改count */}
+      <button onClick={e => setCount(count + 1)}> +1 </button>
      <button></button>
    </div>
  )
})

你会发现上面的代码差异非常大:

  • 函数式组件结合hooks让整个代码变得非常简洁

  • 并且再也不用考虑this相关的问题 ;

内置Hook

useState()

  • useState(initialState?)返回:[state, setState],用于在函数组件中管理状态(state)
    • 参数
    • initialState?any,状态的初始值,不设置则为undefined
    • 返回
    • state:``,当前状态值,第一次调用为初始化值
    • setStateFunction,用于更新状态值的函数。当使用这个函数设置新的状态时,React会根据新的状态重新渲染组件。

useState解析

那么我们来研究一下核心的一段代码代表什么意思:

  • useState来自react,需要从react中导入,它是一个hook;

    • 参数:初始化值,如果不设置为undefined;

    • 返回值:数组,包含两个元素;

      • 元素一:当前状态的值(第一次调用为初始化值);
      • 元素二:设置状态值的函数;
  • 点击button按钮后,会完成两件事情:

    • 调用setCount设置一个新的值
    • 组件重新渲染,并且根据新的值返回DOM结构;

相信通过上面的一个简单案例,你已经会喜欢上Hook的使用了。

  • Hook 就是 JavaScript 函数,这个函数可以帮助你 钩入(hook into) React State以及生命周期等特性;

但是使用它们会有两个额外的规则

  • 只能在函数最顶层调用 Hook。不要在循环条件判断或者子函数中调用。

  • 只能在 React 的函数组件或自定义Hook中调用 Hook。不要在其他 JavaScript 函数中调用。

image-20230418143429904

Tip:

  • Hook指的类似于useState、useEffect这样的函数
  • Hooks是对这类函数的统称;

认识useState

State Hook的API就是 useState,我们在前面已经进行了学习:

  • useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。

    • 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
  • useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为undefined)。

  • useState的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。

FAQ:为什么叫 useState 而不叫 createState?

  • “create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。

  • 在下一次重新渲染时,useState 返回给我们当前的 state。

  • 如果每次都创建新的变量,它就不是 “state”了。

  • 这也是 Hook 的名字总是以 use 开头的一个原因。

当然,我们也可以在一个组件中定义多个变量和复杂变量(数组、对象)

useEffect()

  • useEffect(callback, dependencies?)返回:,用于在函数组件中执行副作用操作,例如访问API、订阅事件、设置定时器等,可以模拟生命周期
    • 参数
    • callback() => void | callbackFn,要执行的副作用代码,可以是异步的或同步的
      • 返回
      • EffectCallback,在useEffect()函数中返回一个函数,这个函数将在组件卸载时执行。
    • dependencies?[],包含了与回调函数有关的依赖项,当这些依赖项发生变化时,才会重新执行回调函数。如果该数组为空,则仅在组件挂载和卸载时运行一次副作用代码。

基本使用

目前我们已经通过hook在函数式组件中定义state,那么类似于生命周期这些呢?

  • Effect Hook 可以让你来完成一些类似于class中生命周期的功能

  • 事实上,类似于网络请求手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);

  • 所以对于完成这些功能的Hook被称之为 Effect Hook;

假如我们现在有一个需求:页面的title总是显示counter的数字,分别使用class组件和Hook实现:

类组件

js
export class App extends PureComponent {
  componentDidMount() {
    // 1. 首次渲染时在title显示
+    document.title = this.state.count
  }
  componentDidUpdate() {
    // 2. 每次更新时在title显示
+    document.title = this.state.count
  }
  constructor() {
    super()
    this.state = {
      count: 100
    }
  }
  render() {
    const { count } = this.state
    return (
      <div>
        <div>Counter: {count}</div>
        <button onClick={e => this.setState({ count: count + 1 })}> +1 </button>
      </div>
    )
  }
}

Hook

js
const App = memo(() => {
  const [ count, setCount ] = useState(200)
  
  // 1. 在title显示count
  // 当前传入的函数会在组件被渲染后自动执行
+  useEffect(() => {
+    document.title = count
+  },[count])

  return (
    <div>
      <div>计数:{count}</div>
      <button onClick={e => setCount(count + 1)}> +1 </button>
    </div>
  )
})

useEffect的解析:

  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作

  • useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数

  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;

清除机制-返回回调函数

在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除

  • 比如我们之前的事件总线或Redux中手动调用subscribe

  • 都需要在componentWillUnmount有对应的取消订阅;

  • Effect Hook通过什么方式来模拟componentWillUnmount呢?

useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B:

js
type EffectCallback = () => (void | (() => void | undefined));

为什么要在 effect 中返回一个函数?

  • 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;

  • 如此可以将添加和移除订阅的逻辑放在一起

  • 它们都属于 effect 的一部分;

React 何时清除 effect?

  • React 会在组件更新卸载的时候执行清除操作

  • 正如之前学到的,effect 在每次渲染的时候都会执行;

js
// useEffect-清除机制
const App = memo(() => {
  const [ count, setCount ] = useState(100)

  useEffect(() => {
    function clickHandle() {
      console.log('触发点击事件~')
    }
+    document.addEventListener('click', clickHandle)

+    return () => {
+      document.removeEventListener('click', clickHandle)
+    }
  })
  return (
    <div>
      <div>App</div>
      <button onClick={e => setCount(count + 1)}>监听点击</button>
    </div>
  )
})

逻辑分离-多个Effect使用

使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:

  • 比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;

使用Effect Hook,我们可以将它们分离到不同的useEffect中:

  • 代码不再给出

Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:

  • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect;

注意:

  • useEffect()函数的执行是异步的,它不会阻塞组件的渲染。
  • 如果有多个useEffect()函数,它们的执行顺序是不确定的,因此应该避免在多个useEffect()函数中使用相同的依赖项,以免造成不可预期的结果。
js
const App = memo(() => {
  const [ count, setCount ] = useState(100)

  // 执行多个useEffect
+  useEffect(() => {
    document.title = count
    console.log('执行:title显示count')
  })

+  useEffect(() => {
    console.log('执行:监听redux store')
  })

+  useEffect(() => {
    console.log('执行:监听事件监听')
  })

  return (
    <div>
      <div>Counter: {count}</div>
      <button onClick={e => setCount(count + 1)}> +1 </button>
    </div>
  )
})

性能优化

默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

  • 某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅);

  • 另外,多次执行也会导致一定的性能问题;

我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?

  • useEffect实际上有两个参数:

  • 参数一:执行的回调函数;

  • 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)

但是,如果一个函数我们希望依赖任何的内容时,也可以传入一个空的数组[]

  • 那么这里的两个回调函数分别对应的就是componentDidMountcomponentWillUnmount生命周期函数了;

示例:性能优化

  • 传入空数组,useEffect只在组件挂载和卸载时执行
  • 传入依赖的变量,useEffect会在变量改变时重新执行
  • 不传入第二个参数,useEffect会在组件每次更新时重新执行
js
const App = memo(() => {
  const [ count, setCount ] = useState(100)
  const [ msg, setMsg ] = useState('Hi')

  // 1. 不传入第二个参数,useEffect会在组件每次更新时重新执行
  useEffect(() => {
    console.log('不传入第二个参数')
+  })

  // 2. 传入空数组,useeffect会在组件挂载和卸载时执行
  useEffect(() => {
    console.log('传入空数组,useeffect会在组件挂载和卸载时执行')
 +  },[])

  // 3. 传入依赖变量,useEffect会在变量改变时重新执行
  useEffect(() => {
    console.log('传入依赖变量,useEffect会在变量改变时重新执行')
 + },[count])

  return (
    <div>
      <div>计数:{count}</div>
      <button onClick={e => setCount(count + 1)}> +1 </button>
      <button onClick={e => setMsg('你好')}> 修改msg </button>
    </div>
  )
})

useContext()

useContext(MyContext)返回:value,可以实现组件之间的数据共享。让你在组件树中传递数据,而不需要手动的将props一级一级地传递下去。

  • 参数
  • MyContextContext,一个Context对象,它可以通过React.createContext()来创建。
  • 返回
  • value:``,返回该Context对象的当前值。

在之前的开发中,我们要在组件中使用共享的Context有两种方式:

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取this.context

  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;

但是多个Context共享时的方式会存在大量的嵌套

  • Context Hook允许我们通过Hook来直接获取某个Context的值;

image-20230404115713865

注意事项:

  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。

基本使用

1、创建Context

js
import { createContext } from "react";
const UserContext = createContext()
export default UserContext

2、提供待共享的value

js
const App = memo(() => {
  return (
    <div>
      <h3>App</h3>
+      <UserContext.Provider value={{name: 'Tom', age: 20, gender: 'male'}}>
+        <Home />
+      </UserContext.Provider>
    </div>
  )
})

3、在子孙组件通过 useContext 获取Context值

js
const HomeBanner = memo(() => {
+  const user = useContext(UserContext)
  return (
    <div>
      <h3>HomeBanner</h3>
      <div className="show">姓名:{user.name}</div>
      <div className="show">年龄:{user.age}</div>
      <div className="show">性别:{user.gender}</div>
    </div>
  )
})

PS、之前获取Context的方法

  • 通过 <XxxContext.Consumer> 获取Context值

    js
    const HomeProduct = memo(() => {
      return (
        <div>
          <h3>HomeProduct</h3>
    +      <UserContext.Consumer>
            {
    +          value => {
                return (
                  <>
                    <div>名称:{value.name}</div>
                    <div>性别:{value.gender}</div>
                    <div>年龄:{value.age}</div>
                  </>
                )
              }
            }
    +      </UserContext.Consumer>
        </div>
      )
    })
  • 通过 ClassName.contextType = XxxContext 获取Context值

    js
      export class HomeList extends PureComponent {
        render() {
    +      const user = this.context
          console.log(user)
          return (
            <>
              <h3>HomeList</h3>
              <div>名称:{user.name}</div>
              <div>性别:{user.gender}</div>
              <div>年龄:{user.age}</div>
            </>
          )
        }
      }
    +  HomeList.contextType = UserContext

useReducer()

  • useReducer(reducer, initialState)返回:[state, dispatch],提供了一种可预测且可测试的方式来处理状态更新逻辑。
    • 参数
    • reducer(state, action) => void,用于根据当前状态和传递的 action 返回新的状态
    • initialStateany,状态的初始值
    • 返回
    • state:``,当前状态
    • dispatch(action) => void,用来分发 action 来触发状态更新

很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是。

useReducer仅仅是useState的一种替代方案

  • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;

    image-20230418172938713

  • 或者这次修改的state需要依赖之前的state时,也可以使用;

示例:基础使用

js
const App = memo(() => {
+  function counterReducer(state, action) {
    switch(action.type) {
      case 'add_number':
        return { ...state, counter: state.counter + action.payload }
      case 'sub_number':
        return { ...state, counter: state.counter - action.payload }
      case 'add_friend':
        return { ...state, friend: [ ...state.friend, action.payload ] }
      default:
        return state
    }
  }
  
+  const [state, dispatch] = useReducer(counterReducer, { counter: 100, friend: ['李雷', '韩梅梅'] })
  return (
    <div>
      <h3>App</h3> 
      <div className="show">
        <div>Counter: {state.counter}</div>
+        <button onClick={e => dispatch({type: 'add_number', payload: 10})}> +10 </button>
+        <button onClick={e => dispatch({type: 'sub_number', payload: 10})}> -10 </button>
        <br />
        <div>朋友:</div>
        <ul>
          {
            state.friend.map((item, index) => {
              return <li key={index}>{item}</li>
            })
          }
        </ul>
+        <button onClick={e => dispatch({type: 'add_friend', payload: '张飞'})}>添加朋友</button>
      </div>
    </div>
  )
})

数据是不会共享的,它们只是使用了相同的counterReducer的函数而已。

所以,useReducer只是useState的一种替代品,并不能替代Redux。

useCallback()

  • useCallback(callback, deps?)返回:memoizedCallback,用于返回一个memoized(记忆化)的回调函数。可用于性能优化
    • 参数
    • callbackfunction,要记忆化的回调函数
    • deps?array,是一个依赖数组,当依赖数组中的任意一个值发生变化时,callback就会重新生成。
    • 返回
    • memoizedCallbackfunction,memoized (记忆化)的回调函数

useCallback实际的目的是为了进行性能的优化。

如何进行性能的优化呢?

  • useCallback会返回一个函数的 memoized (记忆的) 值;

  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

image-20230404115815740

案例

  • 案例一:使用useCallback和不使用useCallback定义一个函数是否会带来性能的优化;
  • 案例二:使用useCallback和不使用useCallback定义一个函数传递给子组件是否会带来性能的优化;

通常使用useCallback的目的不希望子组件进行多次渲染,并不是为了函数进行缓存

useCallback性能优化的点:

  • 1、当需要将一个函数传递给子组件时,最好用useCallback进行优化,将优化之后的函数,传递给子组件

闭包陷阱

说明:bar1,bar2是由函数foo内部返回的函数,该函数中count的值是在定义的时刻就固定的

js
  // 演示闭包陷阱 
  function foo(count) {
    return function() {
      console.log(count + 1)
    }
  }
  const bar1 = foo(100)
  const bar2 = foo(200)

  + bar1() // 101
  bar2() // 201

示例:防止子组件被重复渲染

  • 普通函数:子组件被重新渲染
  • 缓存函数-不传参数:子组件被重新渲染
  • 缓存函数-传递空数组:子组件没有被重新渲染
  • 缓存函数-传递依赖变量:子组件被重新渲染
  • 缓存函数-传递依赖变量,但修改另一个变量:子组件没有被重新渲染
js
const App = memo(() => {
  const [ count, setCount ] = useState(100)
  const [ msg, setMsg ] = useState('Hi')

  // 1. 普通函数:子组件被重新渲染
  const increment = () => {
    setCount(count + 1)
    console.log('执行普通increment函数~')
  }

  // 2. 缓存函数-不传参数:子组件被重新渲染
  const increment = useCallback(() => {
    setCount(count + 1)
    console.log('执行-不传参数的-缓存increment函数~~')
  })

  // 3. 缓存函数-传递空数组:子组件没有被重新渲染
  const increment = useCallback(() => {
    setCount(count + 1)
    console.log('执行-传递空数组-缓存increment函数~~')
  },[])

  // 4. 缓存函数-传递依赖变量:子组件被重新渲染
  const increment = useCallback(() => {
    setCount(count + 1)
    console.log('执行-传递依赖变量-缓存increment函数~~')
  },[count])

  // 5. 缓存函数-传递依赖变量,但修改另一个变量:子组件没有被重新渲染
  const increment = useCallback(() => {
    setCount(count + 1)
    console.log('执行-传递依赖变量-缓存increment函数~~')
  },[count])

  return (
    <div>
      <h3>App Counter: {count}</h3>
      <h3>App Msg: {msg}</h3>
      <button onClick={increment}> +1 </button>
      <button onClick={e => setMsg('你好')}> 修改msg </button>

      <Home increment={increment}/>
    </div>
  )
})

示例:性能优化-count值变化时子组件依然不会重新渲染

说明:

  • 通过传递useCallback的deps参数空数组[],保证foo函数每次渲染都是同一个函数
  • 既然foo函数每次父组件渲染时保持不变,那么子组件就不会被重新渲染
  • 通过对象变量countRef保存依赖的数据,保证在一个固定的函数中每次可以取到count的最新值,因为对象是引用值
js
const App = memo(() => {
  const [ count, setCount ] = useState(100)
  const countRef = useRef()
  countRef.current = count
  const increment = useCallback(function foo() {
    setCount(countRef.current + 1)
    console.log('执行increment函数~')
  }, [])
  
  return (
    <div>
      <h3>App Counter: {count}</h3>
      <button onClick={increment}> +1 </button>

      <Home increment={increment}/>
    </div>
  )
})

useMemo()

  • useMemo(callback, deps)返回:memoizedValue,用于缓存计算结果,以便在依赖项未更改时避免不必要的重新计算。
    • 参数
    • callbackFunction,计算逻辑的回调函数
    • depsArray,是一个依赖数组,当依赖项数组中的任何一个值发生变化时,useMemo()会重新计算并返回新的计算结果。如果依赖项数组为空,则该函数仅在组件首次渲染时被计算。
    • 返回
    • memoizedValueany,memoized(记忆化)的计算值

useMemo实际的目的也是为了进行性能的优化

如何进行性能的优化呢?

  • useMemo返回的也是一个 memoized(记忆的) 值;

  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

image-20230404115845968

案例:

  • 案例一:进行大量的计算操作,无须每次渲染时都重新计算;

  • 案例二:对子组件传递相同内容的对象时,使用useMemo进行性能的优化

useMemo和useCallback的对比

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

示例:进行大量的计算操作,无须每次渲染时都重新计算

js
const App = memo(() => {
  const [count, setCount] = useState(100)

  function calNum(num) {
    console.log('执行计算calNum')
    let total = 0
    for(let i=1; i<=num; i++) {
      total += i
    }
    return total
  }

  // 1. 普通函数:每次重新渲染,都执行一次计算
  const res = calNum(count)

  // 2. 缓存计算结果:依赖count变化时重新执行计算
+  const res = useMemo(() => {
+    return calNum(count)
+  },[count])

  return (
    <div>
      <h3>App res1: {res} - {count}</h3>

      <button onClick={e => setCount(count + 1)}> +1 </button>
    </div>
  )
})

示例:对子组件传递相同内容的对象时,使用useMemo对子组件渲染进行优化

对子组件传递相同内容的对象时,父组件重新渲染时,子组件也会重新渲染

原因: 父组件重新渲染时,每次都会定义一个新的对象,2次定义的对象不是同一个对象

*优化:*使用useMemo进行性能的优化

js
const App = memo(() => {
  const [ count, setCount ] = useState(100)

  
  // 1. 传递一个值类型到子组件,value不变时子组件不会重新渲染
  const value = 100

  // 2. 传递一个引用类型到子组件,info每次都会创建一个新对象,子组件会被重新渲染
  const info = { name: 'Jack', age: 20 }

  // 3. 通过useMemo返回一个不变的对象,子组件不会被重新渲染
+  const obj = useMemo(() => ({ name:'Tom', age: 33 }), [])

  return (
    <div>
      <h3>App</h3>
      <button onClick={e => setCount(count + 1)}> +1 </button>
      <hr />
      <Home value={value}/>
      <Profile info={info}/>
+      <About obj={obj} />
    </div>
  )
})

useRef()

  • useRef(initialValue?)返回:refContainer,用于创建一个可变的引用,它返回一个对象,对象中有一个 current 属性,该属性的值可以在组件的整个生命周期中保持不变,并可以被读取和修改。
    • 参数
    • initialValue?any,表示 refContainer.current 的初始值
    • 返回
    • refContainer{current},refContainer对象中有一个 current 属性,该属性的值可以在组件的整个生命周期中保持不变,并可以被读取和修改。

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。

image-20230404115906565

ref的2种用法:

用法一绑定DOM(或者组件,但是需要是class组件)元素;

示例

js
const App = memo(() => {
+  const titleRef = useRef()

  const getDOM = () => {
    console.log(titleRef) // {current: h3}
+    console.log(titleRef.current) // <h3>App</h3>
    console.log(titleRef.current.innerHTML) // App
  }

  return (
    <div>
+      <h3 ref={titleRef}>App</h3>
      <button onClick={getDOM}>获取DOM</button>
    </div>
  )
})

用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;

验证useRef的返回值在组件重新渲染时始终是同一个对象

js
let obj = null
const App = memo(() => {
  const [count, setCount] = useState(100)

  // 判断2次渲染时的ref对象是否是同一个
+  const nameRef = useRef('Tom')
+  console.log('obj === nameRef? ', obj === nameRef) // obj === nameRef?  true
+  obj = nameRef

  return (
    <div>
      <h3>App</h3>
      <button onClick={e => setCount(count + 1)}> +1 </button>
    </div>
  )
})

useImperativeHandle()

  • useImperativeHandle(ref, createHandle, deps?)返回:,用于向父组件暴露子组件的实例方法
    • 参数
    • refObject | (cpn) => void,如果 ref 参数是函数,则函数的第一个参数为子组件的实例对象,用于设置 ref 的值。
    • createHandle() => {method,...},用于创建一个对象,该对象包含子组件需要向父组件暴露的方法。
    • deps?Array,包含所有影响 createHandle 函数的值,当这些值发生变化时,会重新调用 createHandle 函数。

useImperativeHandle并不是特别好理解,我们一点点来学习。

我们先来回顾一下ref和forwardRef结合使用:

  • 通过forwardRef可以将ref转发到子组件;

  • 子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;

示例:ref绑定子组件中的某个元素

父组件

js
const App = memo(() => {
+  const homeRef = useRef()
  
  const getCpnElement = () => {
+    console.log(homeRef.current)
+    homeRef.current.focus()
+    homeRef.current.value = '输入姓名'
  }
  return (
    <div>
      <h3>App</h3>
      <button onClick={e => getCpnElement()}>获取子组件元素</button>
      <hr />
+      <Home ref={homeRef}/>
    </div>
  )
})

子组件

js
+ const Home = memo(forwardRef((props, ref) => {
  return (
    <div>
      <div className="title">Home</div>
+      <input type="text" ref={ref}/>
    </div>
  )
}))

forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:

  • 直接暴露给父组件带来的问题是某些情况的不可控;

  • 父组件可以拿到DOM后进行任意的操作;

  • 但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作

通过useImperativeHandle可以值暴露固定的操作:

  • 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起;

  • 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象;

  • 比如我调用了 focus函数,甚至可以调用 printHello函数;

示例:使用useImperativeHandle限制父组件只能操作子组件暴露的指定方法

父组件

image-20230419172535367

子组件

  • 父组件传递的ref别绑定到了useImperativeHandle上
  • 子组件中通过useImperativeHandle定义要向父组件暴露的函数
  • 在子组件中另外定义 inputRef 获取DOM元素
js
const Profile = memo(forwardRef((props, ref) => {
+  const inputRef = useRef()
  // 定义要向父组件暴露的函数
+  useImperativeHandle(ref, () => {
+    return {
+      focus() {
+        inputRef.current.focus()
+      }
+    }
+  })
  return (
    <div>
      <h3>Profile</h3>
+      <input type="text" ref={inputRef}/>
    </div>
  )
}))

useLayoutEffect()

  • useLayoutEffect(effect, deps)返回:,与 useEffect() 类似,但是它会在浏览器 layout 和 paint 之前执行,因此可以在渲染前同步读取 DOM 布局和触发重渲染。
    • 参数
    • effect() => void | Function,用于执行副作用操作,可以返回一个清除函数。
    • depsArray,包含所有影响 effect 函数的值,当这些值发生变化时,会重新调用 effect 函数。

useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:

  • useEffect会在渲染的内容更新到DOM上之后执行,不会阻塞DOM的更新;

  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;

如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect

image-20230404120023929

官方更推荐使用useEffect而不是useLayoutEffect。

示例: useEffect和useLayoutEffect的对比

useEffect: 先显示了0后,再在useEffect中修改为随机值,会有闪烁现象

js
const App = memo(() => {
  const [count, setCount] = useState(100)

+  useEffect(() => {
+    if(count > 100) {
+      setCount(1)
+    }
+  },[count])
   return (
    <div>
      <h3>App Counter: {count}</h3>
      <button onClick={e => setCount(count + 1)}> +1 </button>
    </div>
  )
})

useLayoutEffect: 没有闪烁现象

js
const App = memo(() => {
  const [count, setCount] = useState(100)

+  useLayoutEffect(() => {
+    if(count > 100) {
+      setCount(1)
+    }
+  }, [count])

  return (
    <div>
      <h3>App Counter: {count}</h3>
      <button onClick={(e) => setCount(count + 1)}> +1 </button>
    </div>
  )
})

自定义Hooks

自定义Hook

自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。

自定义Hook必须以use开头

需求:所有的组件在创建和销毁时都进行打印

  • 组件被创建:打印“组件被创建了";

  • 组件被销毁:打印"组件被销毁了";

image-20230404120039955

抽取代码

1、定义Hook

js
import { useEffect } from "react";

function useLifeLog(cpnName) {
  useEffect(() => {
    console.log(cpnName + ' 组件被创建~')

    return () => {
      console.log(cpnName + ' 组件被销毁~')
    }
  }, [])
}

export default useLifeLog

2、使用自定义Hook

js
// Home.jsx
const Home = memo(() => {
   // 使用自定义Hook 
+  useLifeLog('Home')

    return (
    <div>Home</div>
  )
})

自定义Hook练习

需求一:Context的共享

1、创建Context

js
import { createContext } from "react";

const UserContext = createContext()
const TokenContext = createContext()

export { UserContext, TokenContext }

2、挂载Context到组件树

js
      <UserContext.Provider value={{name: 'Tom', age: 19}}>
        <TokenContext.Provider value={{token: 'token~'}}>
          <Home />
        </TokenContext.Provider>
      </UserContext.Provider>

3、自定义Hook

js
import { useContext } from "react";
import { TokenContext, UserContext } from "../context";

function useUserInfo() {
  const user = useContext(UserContext)
  const token = useContext(TokenContext)
  
  return [user, token]
}

export default useUserInfo

4、使用hook

js
const HomeBanner = memo(() => {
+  const [ user, token ] = useUserInfo()
  return (
    <div>
      <div>HomeBanner</div>
      <div>姓名:{user.name}</div>
      <div>年龄:{user.age}</div>
      <div>Token:{token.token}</div>
    </div>
  )
})

需求二:获取滚动位置

js
import { useEffect, useState } from "react"

function useScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState({x: 0, y: 0})

  useEffect(() => {
    // 事件监听函数
    function scrollHandler() {
      setScrollPosition({
        x: window.scrollX,
        y: window.scrollY
      })
    }
    // 监听滚动事件
    document.addEventListener('scroll', scrollHandler)
    return () => {
      // 取消监听滚动事件
      document.removeEventListener('scroll', scrollHandler)
    }
  }, [])
  return scrollPosition
}

export default useScrollPosition

需求三:localStorage数据存储

1、定义hook

通过key,直接从localStorage中获取一个数据

js
import { useEffect, useState } from "react"

function useLocalStorage(key) {
  const [data, setData] = useState(() => {
    return JSON.parse(localStorage.getItem(key))
  })

  // 保存到本地存储是副作用
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data))
    
  }, [key, data])

  return [data, setData]
}

export default useLocalStorage

2、使用hook

js
const App = memo(() => {
+  const [token, setToken] = useLocalStorage('token')
+  const [user, setUser] = useLocalStorage('user')
  return (
    <div>
+      <div>token: {token}</div>
+      <button onClick={e => setToken('令牌')}>设置token</button>

      <hr />
+      <div>name: {user?.name}, age: {user?.age}</div>
+      <button onClick={e => setUser({ name: '张飞', age: 48 })}>设置user</button>

    </div>
  )
})

redux hooks

  • useSelector(selectorFn, equalityFn?)返回:selectedState,用于从 Redux store 中获取 state。
    • 参数
    • selectorFn(state) => selectedState,接收整个 Redux store 的 state 作为参数,返回需要获取的 state。
    • equalityFn?Function,比较返回的两个对象是否相等来决定是否组件重新渲染
    • 返回
    • selectedStateState,Redux store 中的 state
  • useDispatch()返回:dispatch,用于获取 dispatch 函数,从而可以向 Redux store 发送 action。
    • 返回
    • dispatch(action) => void,dispatch 函数,可以用来发送 action
  • useStore()返回:store,用于获取 Redux store 对象。它返回整个 Redux store 对象,可以用来获取、设置 state,以及订阅 state 的变化。
    • 返回
    • storeObject,整个 Redux store 对象

之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect

  • 但是这种方式必须使用高阶函数结合返回的高阶组件;

  • 并且必须编写:mapStateToProps和 mapDispatchToProps映射的函数;

Redux7.1开始,提供了Hook的方式,我们再也不需要编写connect以及对应的映射函数了

useSelector作用将state映射到组件中:**

  • 参数一:将state映射到需要的数据中;

  • 参数二:可以进行比较来决定是否组件重新渲染;(后续讲解)

useSelector默认会比较我们返回的两个对象是否相等;

  • 如何比较呢? const refEquality = (a, b) => a === b;

  • 也就是我们必须返回两个完全相等的对象才可以不引起重新渲染;

useDispatch非常简单,就是直接获取dispatch函数,之后在组件中直接使用即可;**

我们还可以通过useStore获取当前的store对象

示例:useSelector(),useDispatch()

1、创建reducer片段

js
const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    count: 100,
    msg: 'hi'
  },
  reducers: {
    addNumber(state, { payload })  {
      state.count = state.count + payload
    },
    subNumber(state, { payload }) {
      state.count = state.count - payload
    },
    changeMsg(state, { payload }) {
      state.msg = payload
    }
  }
})

export const { addNumber, subNumber, changeMsg } = counterSlice.actions
export default counterSlice.reducer

2、定义Redux store

js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from './features/counter'

const store = configureStore({
  reducer: {
    counter: counterReducer
  }
})

export default store

3、通过Provider提供store

js
root.render(
+  <Provider store={store}>
    <App />
  </Provider>
);

4、获取store数据

*之前:*在组件中通过connect()获取store数据

image-20230420102555276

image-20230420102344030

优化: 使用Hook获取,修改store数据

在组件中通过 useSelector() 获取store数据

在组件中通过 useDispatch() 获取dispatch,再通过dispatch派发action修改store数据

js
const App = memo(() => {
    // 获取store数据
+  const { count, msg } = useSelector((state) => ({
+    count: state.counter.count, 
+    msg: state.counter.msg
+  }))
    // 获取dispatch
+  const dispatch = useDispatch()

  return (
    <div>
+      <div>App Counter: {count}</div>
+      <button onClick={e => dispatch(addNumber(1))}> +1 </button>
      <hr />
+      <div>msg: {msg}</div>
+      <button onClick={e => dispatch(changeMsg('你好'))}> 修改msg </button>
    </div>
  )
})

优化2: 全等比较(浅层)和更新,让组件不用监听整个store的变化,只监听本组件用到的store数据是否发生变化

在store中增加message

image-20230420103757142

在Home中展示message,并通过打印观察是否重新渲染

image-20230420104240527

说明:由于useSelector监听的是整个state,当state中某个值改变时,就会重新渲染所在组件

在useSelector的第二参数中对提取的store数据进行浅层比较(shallowEqual)

js
  const { msg } = useSelector(state => ({
    msg: state.counter.msg
+  }), shallowEqual)

其他Hook

useId()

  • useId(prefix?)返回:id,用于生成横跨服务端和客户端的稳定的唯一ID的同时避免 hydration 不匹配的 hook
    • 参数
    • prefix?string,用于在生成的id前添加前缀。如果不传入prefix,则默认为“id”。
    • 返回
    • idstring,生成的唯一标识符

官方的解释:useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook

概念:hydration

hydration:要想理解这个词,我们需要理解一些服务器端渲染(SSR)的概念。

概念:CSR

CSR:客户端渲染(Client Side Rendering),我们开发的SPA页面通常依赖的就是客户端渲染;

概念:SPA

SPA:单页面富应用(Single Page Application),所有内容都在一个单独的页面上加载,而不需要重新加载整个页面。相反,页面的内容是通过JavaScript动态加载的,使用户可以在不刷新整个页面的情况下浏览不同的内容。

缺点:

  • 不利于SEO:由于所有的内容都在一个页面上,因此对于搜索引擎来说,很难对页面进行精确的索引。
  • 首屏渲染速度慢

概念:SSR

SSR:服务端渲染(Server Side Rendering),指的是页面在服务器端已经生成了完成的HTML页面结构,不需要浏览器通过执行JS代码来生成页面结构;

早期的服务端渲染包括PHP、JSP、ASP等方式,但是在目前前后端分离的开发模式下,前端开发人员不太可能再去学习PHP、JSP等技术来开发网页;

不过我们可以借助于Node来帮助我们执行JavaScript代码,提前完成页面的渲染

image-20230404120202768

SSR同构应用

什么是同构?

  • 一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用

同构是一种SSR的形态,是现代SSR的一种表现形式。

  • 当用户发出请求时,先在服务器通过SSR渲染出首页的内容。

  • 但是对应的代码同样可以在客户端被执行。

  • 执行的目的包括事件绑定等以及其他页面切换时也可以在客户端被渲染;

image-20230404120251060

image-20230404120258297

Hydration

什么是Hydration?这里我引入vite-plugin-ssr插件的官方解释。

image-20230404120311833

在进行 SSR 时,我们的页面会呈现为 HTML。

  • 但仅 HTML 不足以使页面具有交互性。例如,浏览器端 JavaScript 为零的页面不能是交互式的(没有 JavaScript 事件处理程序来响应用户操作,例如单击按钮)。

  • 为了使我们的页面具有交互性除了在 Node.js 中将页面呈现为 HTML 之外,我们的 UI 框架(Vue/React/...)还在浏览器中加载和呈现页面。(它创建页面的内部表示,然后将内部表示映射到我们在 Node.js 中呈现的 HTML 的 DOM 元素。)

这个过程称为hydration

useId的作用

我们再来看一遍:useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。

所以我们可以得出如下结论:

  • useId是用于react的同构应用开发的,前端的SPA页面并不需要使用它;

  • useId可以保证应用程序在客户端和服务器端生成唯一的ID,这样可以有效的避免通过一些手段生成的id不一致,造成hydration mismatch

image-20230420123258341

useTransition()

  • useTransition({timeoutMs})返回:[isPending, startTransition],用于在渲染期间对异步更新进行控制。告诉 react 对于某部分任务的更新优先级较低,可以稍后进行更新。
    • 参数
    • timeoutMsnumber,表示等待异步更新的最长时间,超过这个时间后,异步更新将被强制中止。
    • 返回
    • isPendingboolean,,表示当前是否处于异步更新的等待中。
    • startTransition(callback) => void,用于触发异步更新

官方解释:返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。

  • 事实上官方的说法,还是让人云里雾里,不知所云。

useTransition到底是干嘛的呢?它其实在告诉react对于某部分任务的更新优先级较低,可以稍后进行更新。

示例: 使用 useTransition 优化带有搜索功能的超长名单列表组件

js
const App = memo(() => {
  const fakeNames = useGenerateFakeNames()
  const [ names, setNames ] = useState(fakeNames)
  // 1. 使用useTrasition,返回2个参数
  const [isPending, startTransition] = useTransition()

  function searchHandle(e) {
    // 2. 调用返回的startTransition函数,延迟执行传入的回调函数
    startTransition(() => {
      const kw = e.target.value
      const filteredNames = fakeNames.filter(item => item.includes(kw))
      setNames(filteredNames)
    })
  }
  
  return (
    <div>
      <div className="search">
        <span>搜索:</span>
        <input type="text" onInput={e => searchHandle(e)}/>
      </div>
      <h3>名单:{ isPending ? 'Loading...' : '' }</h3>
      <ul>
        {
          names.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
})

1、生成随机名字

  • 库:faker
  • 安装:npm i @faker-js/faker -D
js
import { faker } from "@faker-js/faker"

function useGenerateFakeNames() {
  const fakeNames = []
  for(let i=0; i<10000; i++) {
+    fakeNames.push(faker.name.firstName()  )
  }
  return fakeNames
}

export default useGenerateFakeNames

2、展示名字列表

js
const App = memo(() => {
+  const fakeNames = useGenerateFakeNames()
+  const [ names, setNames ] = useState(fakeNames)
  return (
    <div>
      <h3>名单:</h3>
      <ul>
        {
+          names.map((item, index) => {
+            return <li key={index}>{item}</li>
+          })
        }
      </ul>
    </div>
  )
})

3、过滤包含输入字符的名字

js
const App = memo(() => {
  const fakeNames = useGenerateFakeNames()
  const [ names, setNames ] = useState(fakeNames)

+  function searchHandle(e) {
+      const kw = e.target.value
+      const filteredNames = fakeNames.filter(item => item.includes(kw))
+      setNames(filteredNames)
+  }
  
  return (
    <div>
      <div className="search">
        <span>搜索:</span>
+        <input type="text" onInput={e => searchHandle(e)}/>
      </div>
      <h3>名单:</h3>
      <ul>
        {
+          names.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
})

问题: 当列表数据很大时,用户操作后会有明显的延迟

4、优化: 使用useTransition将名字列表渲染的优先级降低,优先显示input框效果

js
const App = memo(() => {
  const fakeNames = useGenerateFakeNames()
  const [ names, setNames ] = useState(fakeNames)
  // 1. 使用useTrasition,返回2个参数
+  const [isPending, startTransition] = useTransition()

  function searchHandle(e) {
    // 2. 调用返回的startTransition函数,延迟执行传入的回调函数
+    startTransition(() => {
+      const kw = e.target.value
+      const filteredNames = fakeNames.filter(item => item.includes(kw))
+      setNames(filteredNames)
+    })
  }
  
  return (
    <div>
      <div className="search">
        <span>搜索:</span>
        <input type="text" onInput={e => searchHandle(e)}/>
      </div>
+      <h3>名单:{ isPending ? 'Loading...' : '' }</h3>
      <ul>
        {
          names.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
})

useDeferredValue()

  • useDeferredValue(value, config)返回:deferredValue,接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后
    • 参数
    • valueany,当前状态的值
    • configobject,配置项
      • timeoutMsnumber,定义延迟多少毫秒后开始更新状态的时间,默认为500毫秒。
      • equalsfunction,定义判断两个值相等的回调函数,如果该函数返回true,React会认为两个值相等,不再更新状态。
    • 返回
    • deferredValueany,是一个延迟更新的值

官方解释:useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后

在明白了useTransition之后,我们就会发现useDeferredValue的作用是一样的效果,可以让我们的更新延迟。

js
const App = memo(() => {
  const fakeNames = useGenerateFakeNames()
  const [ names, setNames ] = useState(fakeNames)
+  const deferedNames = useDeferredValue(names)

+  function searchHandle(e) {
+    const kw = e.target.value
+    const filteredNames = fakeNames.filter(item => item.includes(kw))
+    setNames(filteredNames)
+  }
  
  return (
    <div>
      <div className="search">
        <span>搜索:</span>
        <input type="text" onInput={e => searchHandle(e)}/>
      </div>
      <h3>名单:</h3>
      <ul>
        {
+          deferedNames.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
})